﻿/*
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Diagnostics;
using System.Reflection;
using System.Data;

namespace IvyOrm
{
    /// <summary>
    /// Provides RecordQuery, RecordSingle and RecordSingleOrDefault extensions to the DbConnection and DbTransaction object.
    /// </summary>
    public static class RecordQueryExtension
    {
        #region RecordQuery DbConnection
        /// <summary>
        /// Executes the specified command and converts the result set to an array of 
        /// type T with each column in the result set mapping to a property or field in type T.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T[] RecordQuery<T>(this DbConnection connection, string commandText, params object[] args)
            where T : new()
        {
            return RecordQuery<T>(connection, new IvyOrmOptions(), commandText, args);
        }

        /// <summary>
        /// Executes the specified command and converts the result set to an array of 
        /// type T with each column in the result set mapping to a property or field in type T.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T[] RecordQuery<T>(this DbConnection connection, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            if (connection == null)
                throw new ArgumentNullException("connection");

            options = IvyOrmOptions.Defaults(connection, options);

            RecordQueryOptions queryOptions = (RecordQueryOptions)options.RecordQueryOptions;
            ValueConverterFactory defaultConverterFactory = options.ValueConverterFactory;
            Debug.Assert(defaultConverterFactory != null);

            var classInfo = IvyClassInfo.GetClassInfo(typeof(T));
            var behavior = ((queryOptions & RecordQueryOptions.SingleRow) == RecordQueryOptions.SingleRow) ? CommandBehavior.SingleRow : CommandBehavior.Default;

            using (var command = options.DbProviderFactory.CreateCommand())
            using (var commandBuilder = options.DbProviderFactory.CreateCommandBuilder())
            {
                command.Connection = connection;

                ExecuteCommandExtension.prepairCommand(command, commandBuilder, commandText, args);
                Logging.Log(options, TraceEventType.Information, 1879036, "RecordQuery with the following command: \n{0}" + command.CommandText);

                using (var reader = command.ExecuteReader(behavior))
                {
                    Debug.Assert(!reader.IsClosed);
                    return read<T>(reader, options, classInfo, defaultConverterFactory);
                }
            }
        }
        #endregion

        #region RecordQuery DbTransaction
        /// <summary>
        /// Executes the specified command and converts the result set to an array of 
        /// type T with each column in the result set mapping to a property or field in type T.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T[] RecordQuery<T>(this DbTransaction transaction, string commandText, params object[] args)
            where T : new()
        {
            return RecordQuery<T>(transaction, new IvyOrmOptions(), commandText, args);
        }

        /// <summary>
        /// Executes the specified command and converts the result set to an array of 
        /// type T with each column in the result set mapping to a property or field in type T.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T[] RecordQuery<T>(this  DbTransaction transaction, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            if (transaction == null)
                throw new ArgumentNullException("transaction");
            if (transaction.Connection == null)
                throw new ArgumentException("must have the connection property set", "transaction");

            options = IvyOrmOptions.Defaults(transaction.Connection, options);
            Debug.Assert(options.RecordQueryOptions != null);
            var recordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions;
            var classInfo = IvyClassInfo.GetClassInfo(typeof(T));
            var behavior = ((recordQueryOptions & RecordQueryOptions.SingleRow) == RecordQueryOptions.SingleRow) ? CommandBehavior.SingleRow : CommandBehavior.Default;
            var defaultConverterFactory = options.ValueConverterFactory;
            Debug.Assert(defaultConverterFactory != null);

            using (var command = options.DbProviderFactory.CreateCommand())
            using (var commandBuilder = options.DbProviderFactory.CreateCommandBuilder())
            {
                command.Connection = transaction.Connection;
                command.Transaction = transaction;

                ExecuteCommandExtension.prepairCommand(command, commandBuilder, commandText, args);
                Logging.Log(options, TraceEventType.Information, 1879036, "RecordQuery with the following command: \n{0}" + command.CommandText);

                using (var reader = command.ExecuteReader(behavior))
                {
                    return read<T>(reader, options, classInfo, defaultConverterFactory);
                }
            }
        }
        #endregion

        

        #region RecordSingle DbConnection
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond an IvyOrmException is thrown
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingle<T>(this  DbConnection connection, string commandText, params object[] args)
            where T : new()
        {
            return RecordSingle<T>(connection, (IvyOrmOptions)null, commandText, args);
        }
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond an IvyOrmException is thrown
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingle<T>(this  DbConnection connection, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            options = IvyOrmOptions.Defaults(connection, options);
            options.RecordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions | RecordQueryOptions.SingleRow;
            var results = RecordQuery<T>(connection, options, commandText, args);
            if (results.Length == 0)
            {
                throw new IvyOrmException("Expecting one or more results, no results returned");
            }

            return results[0];
        }
        #endregion

        #region RecordSingle DbTransaction
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond an IvyOrmException is thrown
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingle<T>(this  DbTransaction transaction, string commandText, params object[] args)
            where T : new()
        {
            return RecordSingle<T>(transaction, new IvyOrmOptions(), commandText, args);
        }
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond an IvyOrmException is thrown
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingle<T>(this  DbTransaction transaction, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            if (transaction == null)
                throw new ArgumentNullException("transaction");
            if (transaction.Connection == null)
                throw new ArgumentException("must have the connection property set", "transaction");

            options = IvyOrmOptions.Defaults(transaction.Connection, options);
            options.RecordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions | RecordQueryOptions.SingleRow;
            var results = RecordQuery<T>(transaction, options, commandText, args);
            if (results.Length == 0)
                throw new IvyOrmException("Expecting one or more results, no results returned");

            return results[0];
        }
        #endregion



        #region RecordSingleOrDefault DbConnection
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond a default(T) is returned.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingleOrDefault<T>(this  DbConnection connection, string commandText, params object[] args)
            where T : new()
        {
            return RecordSingleOrDefault<T>(connection, new IvyOrmOptions(), commandText, args);
        }
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond a default(T) is returned.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="connection">Connection to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingleOrDefault<T>(this  DbConnection connection, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            options = IvyOrmOptions.Defaults(connection, options);
            options.RecordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions | RecordQueryOptions.SingleRow;
            var results = RecordQuery<T>(connection, options, commandText, args);

            if (results.Length == 0)
                return default(T);
            else
                return results[0];
        }
        #endregion

        #region RecordSingleOrDefault DbTransaction
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond a default(T) is returned.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingleOrDefault<T>(this  DbTransaction transaction, string commandText, params object[] args)
            where T : new()
        {
            return RecordSingleOrDefault<T>(transaction, new IvyOrmOptions(), commandText, args);
        }
        /// <summary>
        /// Executes the specified command and converts the result to an object of type T with each column in 
        /// the result set mapping to a property or field in type T. If no results are fond a default(T) is returned.
        /// </summary>
        /// <typeparam name="T">The target value to return. Must be a class or a struct</typeparam>
        /// <param name="transaction">Transaction to execute the command on.</param>
        /// <param name="options">
        /// Specifies additional options to be used with the operation.
        /// </param>
        /// <param name="commandText">
        /// The command to execute including parameters placeholders formatted as @0, @1, ... placeholders will be
        /// converted to placeholders compatible with the database.
        /// </param>
        /// <param name="args">An object array that contains zero or more objects to as database parameters values.</param>
        /// <returns>Array of new objects derived from the result set</returns>
        public static T RecordSingleOrDefault<T>(this  DbTransaction transaction, IvyOrmOptions options, string commandText, params object[] args)
            where T : new()
        {
            if (transaction == null)
                throw new ArgumentNullException("transaction");
            if (transaction.Connection == null)
                throw new ArgumentException("must have the connection property set", "transaction");

            options = IvyOrmOptions.Defaults(transaction.Connection, options);
            options.RecordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions | RecordQueryOptions.SingleRow;
            var results = RecordQuery<T>(transaction, options, commandText, args);

            if (results.Length == 0)
                return default(T);
            else
                return results[0];
        }
        #endregion 

        private struct Mapping
        {
            public Mapping(int index, IvyColumnInfo column, Converter<object, object> converter)
            {
                this.Index = index;
                this.Column = column;
                this.Converter = converter;
            }
            public int Index;
            public IvyColumnInfo Column;
            public Converter<object, object> Converter;
        }

        private static T[] read<T>(DbDataReader reader, IvyOrmOptions options, IvyClassInfo classInfo, ValueConverterFactory defaultConverterFactory) where T : new()
        {
            Debug.Assert(!reader.IsClosed);

            RecordQueryOptions recordQueryOptions = (RecordQueryOptions)options.RecordQueryOptions;


            List<Mapping> mappings = new List<Mapping>();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                var fieldName = reader.GetName(i);
                var fieldType = reader.GetFieldType(i);
                IvyColumnInfo columnInfo;
                classInfo.Columns.TryGetValue(fieldName, out columnInfo);
                if (((recordQueryOptions & RecordQueryOptions.ThrowErrorOnExtraFields) == RecordQueryOptions.ThrowErrorOnExtraFields) && columnInfo == null)
                {
                    var exception = new IvyOrmException("Extra field '" + fieldName + "' included in result set");
                    Logging.Log(options, TraceEventType.Error, 0, exception.Message, exception);
                    throw exception;
                }

                if (columnInfo != null)
                {
                    var converter = (columnInfo.ConverterFactory ?? defaultConverterFactory).GetFromDatabaseConverter(fieldType, columnInfo.FieldOrPropertyType);
                    Debug.Assert(converter != null);
                    mappings.Add(new Mapping(i, columnInfo, converter));
                }
            }

            if (((recordQueryOptions & RecordQueryOptions.ThrowErrorOnMissingFields) == RecordQueryOptions.ThrowErrorOnMissingFields) && mappings.Count != classInfo.Columns.Count)
            {
                var exception = new IvyOrmException("One or more fields are missing from the result set");
                Logging.Log(options, TraceEventType.Error, 0, exception.Message, exception);
                throw exception;
            }

            List<T> results = new List<T>();

            while (reader.Read())
            {
                object resultObject = new T();
                foreach (var mapping in mappings)
                {
                    var sourceValue = reader[mapping.Index];
                    if (sourceValue == DBNull.Value)
                        sourceValue = null;

                    var convertedValue = mapping.Converter(sourceValue);

                    var member = mapping.Column.Member;
                    var memberAsFieldInfo = (member as FieldInfo);
                    if (memberAsFieldInfo != null)
                    {
                        memberAsFieldInfo.SetValue(resultObject, convertedValue);
                    }
                    else
                    {
                        var memberAsPropertyInfo = member as PropertyInfo;
                        Debug.Assert(memberAsPropertyInfo != null);
                        memberAsPropertyInfo.SetValue(resultObject, convertedValue, null);
                    }
                }

                results.Add((T)resultObject);

                if ((recordQueryOptions & RecordQueryOptions.SingleRow) == RecordQueryOptions.SingleRow)
                    break;
            }
            return results.ToArray();
        }
    }
}
